/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


// common.cpp -- misc functions used in client and server


#include "core_main.h"
#include <setjmp.h>


#define MAX_ARGS		64

static int		com_argc;
static char *	com_argv[MAX_ARGS];

static char *	com_redirectBuffer;
static int		com_redirectSize;
static int		com_redirectLen;
static void		(*com_redirectFlush)(const char *);

static FILE *	com_logFileHandle;

static bool		com_serverState;

static bool		com_errorEntered;
static char		com_errorMessage[MAX_PRINT_MESSAGE];

static jmp_buf	com_abortFrame;

int				com_frameTime = 0;
int				com_frameMsec = 0;
int				com_frameCount = 0;

int				com_timeAll = 0, com_timeWaiting = 0;
int				com_timeServer = 0, com_timeClient = 0;
int				com_timeFrontEnd = 0, com_timeBackEnd = 0, com_timeSound = 0;

cvar_t *		com_version;
cvar_t *		com_dedicated;
cvar_t *		com_developer;
cvar_t *		com_fixedTime;
cvar_t *		com_timeScale;
cvar_t *		com_viewLog;
cvar_t *		com_speeds;
cvar_t *		com_maxFPS;
cvar_t *		com_logFile;


/*
 ==============================================================================

 CLIENT / SERVER INTERACTIONS

 ==============================================================================
*/


/*
 ==================
 Com_BeginRedirect
 ==================
*/
void Com_BeginRedirect (char *buffer, int size, void (*flush)(const char *)){

	if (!buffer || !size || !flush)
		return;

	com_redirectBuffer = buffer;
	com_redirectSize = size;
	com_redirectLen = 0;
	com_redirectFlush = flush;

	*com_redirectBuffer = 0;
}

/*
 ==================
 Com_EndRedirect
 ==================
*/
void Com_EndRedirect (void){

	if (!com_redirectFlush)
		return;

	if (com_redirectLen)
		com_redirectFlush(com_redirectBuffer);

	com_redirectBuffer = NULL;
	com_redirectSize = 0;
	com_redirectLen = 0;
	com_redirectFlush = NULL;
}

/*
 ==================
 Com_OpenLogFile
 ==================
*/
static void Com_OpenLogFile (void){

	const char	*savePath, *game;
	char		path[MAX_OSPATH];
	time_t		clock;
	struct tm	*curTime;

	if (com_logFileHandle)
		return;

	savePath = CVar_GetVariableString("fs_savePath");
	game = CVar_GetVariableString("fs_game");

	Str_SPrintf(path, sizeof(path), "%s/%s/console.log", savePath, game);

	if (com_logFile->integerValue == 1 || com_logFile->integerValue == 2)
		com_logFileHandle = fopen(path, "wt");
	else if (com_logFile->integerValue == 3 || com_logFile->integerValue == 4)
		com_logFileHandle = fopen(path, "at");

	if (!com_logFileHandle)
		return;

	if (com_logFile->integerValue == 2 || com_logFile->integerValue == 4)
		setvbuf(com_logFileHandle, NULL, _IONBF, 0);

	time(&clock);
	curTime = localtime(&clock);

	fprintf(com_logFileHandle, "%s %s (%s %s)", ENGINE_VERSION, BUILD_STRING, __DATE__, __TIME__);
	fprintf(com_logFileHandle, "\nLog file opened on %s\n\n", asctime(curTime));
}

/*
 ==================
 Com_CloseLogFile
 ==================
*/
static void Com_CloseLogFile (void){

	time_t		clock;
	struct tm	*curTime;

	if (!com_logFileHandle)
		return;

	time(&clock);
	curTime = localtime(&clock);

	fprintf(com_logFileHandle, "\nLog file closed on %s\n\n", asctime(curTime));
			
	fclose(com_logFileHandle);
	com_logFileHandle = NULL;
}

/*
 ==================
 Com_Printf
 ==================
*/
void Com_Printf (const char *fmt, ...){

	char	message[MAX_PRINT_MESSAGE];
	va_list	argPtr;
	int		len;

	va_start(argPtr, fmt);
	len = Str_VSPrintf(message, sizeof(message), fmt, argPtr);
	va_end(argPtr);

	// Redirect print
	if (com_redirectFlush){
		if (len <= 0)
			return;

		if (com_redirectLen + len > com_redirectSize - 1){
			com_redirectFlush(com_redirectBuffer);

			*com_redirectBuffer = 0;
			com_redirectLen = 0;
		}

		Str_Append(com_redirectBuffer, message, com_redirectSize);
		com_redirectLen += len;

		return;
	}

	// Print to client console
	Con_Print(message);

	// Print to dedicated console
	Sys_Print(message);

	// Print to log file
	if (com_logFile){
		if (com_logFile->integerValue)
			Com_OpenLogFile();
		else
			Com_CloseLogFile();

		if (com_logFileHandle)
			fprintf(com_logFileHandle, "%s", message);
	}
}

/*
 ==================
 Com_DPrintf

 A Com_Printf that only shows up in developer mode
 ==================
*/
void Com_DPrintf (const char *fmt, ...){

	char	message[MAX_PRINT_MESSAGE];
	va_list	argPtr;

	if (!com_developer || !com_developer->integerValue)
		return;		// Don't confuse non-developers with techie stuff

	va_start(argPtr, fmt);
	Str_VSPrintf(message, sizeof(message), fmt, argPtr);
	va_end(argPtr);
	
	Com_Printf("%s", message);
}

/*
 ==================
 Com_Error
 ==================
*/
void Com_Error (bool fatal, const char *fmt, ...){

	static int	count, lastTime;
	int			time;
	va_list		argPtr;

	// If we are getting a stream of errors, do a fatal error
	time = Sys_Milliseconds();

	if (time - lastTime < 100){
		if (++count == 3)
			fatal = true;
	}
	else
		count = 0;

	lastTime = time;

	// Avoid recursion
	if (com_errorEntered)
		Sys_Error("Recursive error after: %s", com_errorMessage);

	com_errorEntered = true;

	// Get the message
	va_start(argPtr, fmt);
	Str_VSPrintf(com_errorMessage, sizeof(com_errorMessage), fmt, argPtr);
	va_end(argPtr);

	// Handle the error
	if (fatal){
		SV_Shutdown(Str_VarArgs("Server fatal crashed: %s", com_errorMessage));
		CL_Shutdown();

		Sys_Error("%s", com_errorMessage);
	}

	longjmp(com_abortFrame, -1);
}

/*
 ==================
 Com_SetServerState
 ==================
*/
void Com_SetServerState (bool serverState){

	com_serverState = serverState;
}

/*
 ==================
 Com_WriteConfig
 ==================
*/
static void Com_WriteConfig (const char *name){

	fileHandle_t	f;

	FS_OpenFile(name, &f, FS_WRITE);
	if (!f){
		Com_Printf("Couldn't write %s\n", name);
		return;
	}

	FS_Printf(f, "// Generated by " ENGINE_NAME ", do not modify!" LINE_SEPARATOR LINE_SEPARATOR);

	Key_WriteBindings(f);
	CVar_WriteArchiveVariables(f);

	FS_CloseFile(f);
}

/*
 ==================
 Com_Freeze_f

 Just freeze in place for a given number of seconds to test error recovery
 ==================
*/
static void Com_Freeze_f (void){

	int		msec, time;

	if (!com_developer->integerValue){
		Com_Printf("This command can only be used in developer mode\n");
		return;
	}

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: freeze <seconds>\n");
		return;
	}

	msec = SEC2MS(Str_ToFloat(Cmd_Argv(1)));

	time = Sys_GenerateEvents();
	while (1){
		if (Sys_GenerateEvents() - time >= msec)
			break;
	}
}

/*
 ==================
 Com_Error_f

 Just throw a drop or fatal error to test error shutdown procedures
 ==================
*/
static void Com_Error_f (void){

	if (!com_developer->integerValue){
		Com_Printf("This command can only be used in developer mode\n");
		return;
	}

	if (Cmd_Argc() > 1)
		Com_Error(false, "Testing drop error");
	else
		Com_Error(true, "Testing fatal error");
}

/*
 ==================
 Com_Crash_f

 A way to force a bus error for development reasons
 ==================
*/
static void Com_Crash_f (void){

	if (!com_developer->integerValue){
		Com_Printf("This command can only be used in developer mode\n");
		return;
	}

	*(int *)0 = 0x12345678;
}

/*
 ==================
 Com_Quit_f
 ==================
*/
static void Com_Quit_f (void){

	Sys_Quit();
}

/*
 ==================
 Com_WriteConfig_f
 ==================
*/
static void Com_WriteConfig_f (void){

	char	name[MAX_OSPATH];

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: writeConfig <fileName>\n");
		return;
	}

	Str_Copy(name, Cmd_Argv(1), sizeof(name));
	Str_DefaultFileExtension(name, sizeof(name), ".cfg");

	Com_Printf("Writing %s...\n", name);

	Com_WriteConfig(name);
}


/*
 ==============================================================================

 COMMAND LINE PARSING

 ==============================================================================
*/


/*
 ==================
 Com_ParseCommandLine

 Breaks the command line into multiple lines
 ==================
*/
static void Com_ParseCommandLine (char *commandLine){

	bool	inQuote = false;

	while (*commandLine){
		if (*commandLine == '"')
			inQuote = !inQuote;

		if ((*commandLine == '+' && !inQuote) || *commandLine == '\n' || *commandLine == '\r'){
			if (com_argc == MAX_ARGS)
				return;

			com_argv[com_argc++] = commandLine + 1;

			*commandLine = 0;
		}

		commandLine++;
	}
}

/*
 ==================
 Com_AddStartupCommands

 Adds command line parameters as script statements.
 Commands are separated by + signs.

 Returns true if any commands were added, which will keep the default action
 from running.
 ==================
*/
static bool Com_AddStartupCommands (void){

	bool	added = false;
	int		i;

	for (i = 0; i < com_argc; i++){
		if (!com_argv[i] || !com_argv[i][0])
			continue;

		if (Str_ICompareChars(com_argv[i], "set", 3))
			added = true;

		Cmd_AppendText(com_argv[i]);
		Cmd_AppendText("\n");
	}

	return added;
}

/*
 ==================
 Com_StartupVariable

 Searches for command line parameters that are "set" commands.
 If match is not NULL, only that variable will be looked for.
 This is necessary because some variables need to be set before executing
 config files, but we want other parameters to override the settings from the
 config files.
 If once is true, the command will be removed. This is to keep it from being
 executed multiple times.
 ==================
*/
void Com_StartupVariable (const char *match, bool once){

	int		i;

	for (i = 0; i < com_argc; i++){
		if (!com_argv[i] || !com_argv[i][0])
			continue;

		if (Str_ICompareChars(com_argv[i], "set", 3))
			continue;

		Cmd_TokenizeString(com_argv[i]);

		if (!match || !Str_ICompare(Cmd_Argv(1), match)){
			CVar_SetVariableString(Cmd_Argv(1), Cmd_Argv(2));

			if (once)
				com_argv[i] = NULL;
		}
	}
}


// ============================================================================


/*
 ==================
 Com_DropFrame

 Called after an error is thrown
 ==================
*/
static void Com_DropFrame (void){

	Com_Printf(S_COLOR_RED "******************************\n");
	Com_Printf(S_COLOR_RED "ERROR: %s\n", com_errorMessage);
	Com_Printf(S_COLOR_RED "******************************\n");

	// Shutdown the server and drop the client connection
	SV_Shutdown(Str_VarArgs("Server crashed: %s", com_errorMessage));
	CL_Drop(com_errorMessage);

	// Make sure temp memory is never leaked
	Mem_TagFree(TAG_TEMP);

	com_errorEntered = false;
}

/*
 ==================
 Com_PrintStats
 ==================
*/
static void Com_PrintStats (void){

	if (com_speeds->integerValue){
		com_timeClient -= (com_timeFrontEnd + com_timeBackEnd + com_timeSound);

		Com_Printf("frame:%i all:%3i (w:%3i) sv:%3i cl:%3i rf:%3i rb:%3i sn:%3i\n", com_frameCount, com_timeAll, com_timeWaiting, com_timeServer, com_timeClient, com_timeFrontEnd, com_timeBackEnd, com_timeSound);
	}

	// Clear for next frame
	com_timeAll = 0, com_timeWaiting = 0;
	com_timeServer = 0, com_timeClient = 0;
	com_timeFrontEnd = com_timeBackEnd = com_timeSound = 0;
}

/*
 ==================
 Com_Frame
 ==================
*/
void Com_Frame (void){

	static int	lastTime;
	int			msec=0, minMsec=0;
	int			time=0;

	// If an error occurred, drop the entire frame
	if (setjmp(com_abortFrame)){
		Com_DropFrame();
		return;
	}

	if (com_speeds->integerValue)
		time = Sys_Milliseconds();

	// Write config file if needed
	if (CVar_GetModifiedFlags() & CVAR_ARCHIVE){
		CVar_ClearModifiedFlags(CVAR_ARCHIVE);

		Com_WriteConfig(ENGINE_NAME ".cfg");
	}

	// Show or hide the dedicated console
	if (com_viewLog->modified){
		if (!com_dedicated->integerValue)
			Sys_ShowConsole(com_viewLog->integerValue);

		com_viewLog->modified = false;
	}

	// We may want to spin here if things are going too fast
	if (!com_dedicated->integerValue && com_maxFPS->integerValue > 0){
		minMsec = 1000 / com_maxFPS->integerValue;
		if (minMsec < 1)
			minMsec = 1;
	}
	else
		minMsec = 1;

	// Main event loop
	while (1){
		com_frameTime = Sys_GenerateEvents();
		if (lastTime > com_frameTime)
			lastTime = com_frameTime;

		msec = com_frameTime - lastTime;
		if (msec >= minMsec)
			break;

		// If not running a dedicated server, sleep until time enough for a
		// frame has gone by
		if (!com_dedicated->integerValue){
			if (com_speeds->integerValue)
				com_timeWaiting += (minMsec - msec) - 1;

			Sys_Sleep((minMsec - msec) - 1);
		}
	}

	lastTime = com_frameTime;

	// Modify msec
	com_frameMsec = msec;

	if (com_fixedTime->integerValue)
		msec = com_fixedTime->integerValue;
	if (com_timeScale->floatValue != 1.0f)
		msec = (int)(msec * com_timeScale->floatValue);

	if (msec < 1)
		msec = 1;

	// Bump frame count
	com_frameCount++;

	// Release memory allocated by clip models
	CM_ClearDynamicModelInstances();

	// Process commands
	Cmd_ExecuteBuffer();

	// Run server frame
	SV_Frame(msec);

	// Run client frame
	if (!com_dedicated->integerValue){
		//Com_ReadPackets();

		CL_Frame(msec);
	}

	if (com_speeds->integerValue)
		com_timeAll += (Sys_Milliseconds() - time);

	// Print collision statistics
	CM_PrintStats();

	// Print memory manager statistics
	Mem_PrintStats();

	// Print com_speeds statistics
	Com_PrintStats();
}

/*
 ==================
 Com_Init
 ==================
*/
int enet_initialize();
void enet_deinitialize();
void Com_Init (char *commandLine){
	
	if (setjmp(com_abortFrame))
		Sys_Error("Error during initialization: %s", com_errorMessage);

	Com_Printf("%s %s (%s %s)\n", ENGINE_VERSION, BUILD_STRING, __DATE__, __TIME__);

	// Parse the command line
	Com_ParseCommandLine(commandLine);

	// Initialize memory manager
	Mem_Init();

	// Initialize system code
	Sys_Init();

	// Set the byte swapping variables in a portable manner
	Swap_Init();

	// Set an initial random seed
	M_RandSeed(Sys_Milliseconds());

	// Prepare enough of the subsystems to handle commands and variables
	Cmd_Init();
	CVar_Init();

	Key_Init();

	// Initialize file system
	FS_Init();

	// Override the settings from the config files with command line parameters
	Com_StartupVariable(NULL, true);

	// Register our variables and commands
	com_version = CVar_Get("com_version", Str_VarArgs("%s %s (%s %s)", ENGINE_VERSION, BUILD_STRING, __DATE__, __TIME__), CVAR_SERVERINFO | CVAR_ROM, "Engine version");
	com_dedicated = CVar_Get("com_dedicated", "0", CVAR_SERVERINFO | CVAR_INIT, "Dedicated server");
	com_developer = CVar_Get("com_developer", "0", 0, "Developer mode");
	com_fixedTime = CVar_Get("com_fixedTime", "0", CVAR_CHEAT, "Fixed time");
	com_timeScale = CVar_Get("com_timeScale", "1.0", CVAR_CHEAT, "Time scale");
	com_viewLog = CVar_Get("com_viewLog", "0", CVAR_CHEAT, "Show or hide the log console");
	com_speeds = CVar_Get("com_speeds", "0", CVAR_CHEAT, "Show engine speeds");
	com_maxFPS = CVar_Get("com_maxFPS", "0", CVAR_ARCHIVE, "Lock framerate");
	com_logFile = CVar_Get("com_logFile", "0", 0, "Log console messages (1 = write, 2 = unbuffered write, 3 = append, 4 = unbuffered append)");

	Cmd_AddCommand("freeze", Com_Freeze_f, "Freezes the game");
	Cmd_AddCommand("error", Com_Error_f, "Tests an error");
	Cmd_AddCommand("crash", Com_Crash_f, "Crashes the game");
	Cmd_AddCommand("quit", Com_Quit_f, "Quits the game");
	Cmd_AddCommand("writeConfig", Com_WriteConfig_f, "Writes a config file");
	// Initialize collision
	CM_Init();

	// Initialize networking
	enet_initialize();
	//NET_Init();
	//NetChan_Init();

	// Initialize server and client
	SV_Init();
	CL_Init();

	// Add commands from the command line
	if (com_dedicated->integerValue)
		Com_AddStartupCommands();
	else {
		// Show or hide the dedicated console
		Sys_ShowConsole(com_viewLog->integerValue);

		// If the user didn't give any commands, run default action
		if (!Com_AddStartupCommands())
			Cmd_AppendText(DEFAULT_ACTION);
	}

	Com_Printf("======= " ENGINE_NAME " Initialization Complete =======\n");
}

/*
 ==================
 Com_Shutdown
 ==================
*/
void Com_Shutdown (void){

	static bool	isDown;

	if (isDown)
		return;
	isDown = true;

	Cmd_RemoveCommand("freeze");
	Cmd_RemoveCommand("error");
	Cmd_RemoveCommand("crash");
	Cmd_RemoveCommand("quit");
	Cmd_RemoveCommand("writeConfig");

	SV_Shutdown("Server quit");
	CL_Shutdown();

	enet_deinitialize();
//	NET_Shutdown();

	CM_Shutdown();

	FS_Shutdown();

	Key_Shutdown();

	CVar_Shutdown();
	Cmd_Shutdown();

	Sys_Shutdown();

	Mem_Shutdown();

	Com_CloseLogFile();
}
